// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! packrat: a task for caching data.
//!
//! There are several cases where we want a task to always start with the same
//! data; e.g., once `net` has come online and chosen a MAC address, it should
//! always use that MAC address even if the `net` task restarts. Packrat solves
//! this problem by being a place where tasks can store data (in the MAC address
//! case, the sequencer task for the relevant board reads the VPD and populates
//! the MAC address in packrat) that can be read back by any task (e.g., `net`).
//!
//! It is critical that packrat itself never restart, as a restart would cause
//! packrat to lose all the data it should be remembering! We attempt to
//! accomplish this via simplicity:
//!
//! 1. All of packrat's functionality should be straightforward get/set
//!    operations in memory; it makes no hardware accesses. For data that
//!    packrat should have that comes from hardware access (e.g., VPD), some
//!    other task is responsible for accessing hardware and then sending data to
//!    packrat.
//! 2. packrat does no parsing of incoming or outgoing data, other than that
//!    generated by the idol server implementation. It should call no fallible
//!    functions.
//! 3. packrat never calls into any other task, as calling into a task gives the
//!    callee opportunity to fault the caller.
//!
//! ## ereport aggregation
//!
//! When the "ereport" feature flag is enabled, packrat is also responsible for
//! aggregating ereports received from other tasks, as described in [RFD 545].
//! In addition to enabling packrat's "ereport" feature, the RNG driver task
//! must have its "ereport" feature flag enabled, so that it can generate a
//! restart ID and send it to packrat on startup. Otherwise, ereports will never
//! be reported.
//!
//! Other tasks interact with the ereport aggregation subsystem through three
//! IPC operations:
//!
//! - `deliver_ereport`: called by any task which wishes to record an ereport,
//!   with a read-only lease containing the CBOR-encoded ereport data. Packrat
//!   will store the ereport in its buffer, provided that space remains for the
//!   message.
//!
//! - `read_ereports`: called by the `snitch` task, this IPC reads ereports
//!   starting at the requested starting ENA into the provided lease. The
//!   `committed_ena` parameter indicates that all ereports with ENAs earlier
//!   than the provided one have been written to persistent storage, and
//!   packrat may flush them from its buffer, to free memory for new
//!   ereports.
//!
//! - `set_ereport_restart_id`: called by the `rng` task to set the
//!   128-bit random restart ID that uniquely identifies this system's
//!   boot/restart. No ereports will be reported until this IPC has been
//!   called.
//!
//! If the "ereport" feature flag is *not* enabled, packrat's `deliver_ereport`
//! and `read_ereports` IPCs will always fail with
//! `ClientError::UnknownOperation`.
//!
//! [RFD 545]: https://rfd.shared.oxide.computer/rfd/0545
#![no_std]
#![no_main]

use core::convert::Infallible;
use gateway_ereport_messages as ereport_messages;
use idol_runtime::{Leased, LenLimit, NotificationHandler, RequestError};
use ringbuf::{ringbuf, ringbuf_entry};
use static_cell::ClaimOnceCell;
use task_packrat_api::{
    CacheGetError, CacheSetError, EreportReadError, EreportWriteError,
    HostStartupOptions, MacAddressBlock, OxideIdentity,
};
use userlib::RecvMessage;

#[cfg(feature = "gimlet")]
mod gimlet;

#[cfg(feature = "grapefruit")]
mod grapefruit;

#[cfg(feature = "cosmo")]
mod cosmo;

mod spd_data;

#[cfg(feature = "gimlet")]
use gimlet::SpdData;

#[cfg(feature = "cosmo")]
use cosmo::SpdData;

#[cfg(feature = "ereport")]
mod ereport;

#[cfg(not(any(feature = "gimlet", feature = "cosmo")))]
type SpdData = spd_data::SpdData<0, 0>; // dummy type

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)] // not all variants are used, depending on cargo features
enum Trace {
    None,
    MacAddressBlockSet(TraceSet<MacAddressBlock>),
    VpdIdentitySet(TraceSet<OxideIdentity>),
    SetNextBootHostStartupOptions(HostStartupOptions),
    SpdDataUpdate {
        index: u8,
        offset: usize,
        len: u8,
    },
    SpdRemoveEeprom {
        index: u8,
    },
    #[cfg(feature = "ereport")]
    RestartIdSet(TraceSet<u128>),
}

impl From<TraceSet<MacAddressBlock>> for Trace {
    fn from(value: TraceSet<MacAddressBlock>) -> Self {
        Self::MacAddressBlockSet(value)
    }
}

impl From<TraceSet<OxideIdentity>> for Trace {
    fn from(value: TraceSet<OxideIdentity>) -> Self {
        Self::VpdIdentitySet(value)
    }
}

#[cfg(feature = "ereport")]
impl From<TraceSet<ereport_messages::RestartId>> for Trace {
    fn from(value: TraceSet<ereport_messages::RestartId>) -> Self {
        // Turn this into a TraceSet<u128> instead of the newtype so that
        // Humility formats it in a less ugly way.
        Self::RestartIdSet(match value {
            TraceSet::Set(id) => TraceSet::Set(id.into()),
            TraceSet::SetToSameValue(id) => TraceSet::SetToSameValue(id.into()),
            TraceSet::AttemptedSetToNewValue(id) => {
                TraceSet::AttemptedSetToNewValue(id.into())
            }
        })
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum TraceSet<T> {
    // Initial set (always succeeds)
    Set(T),
    // Repeated set, but the same value as we have cached
    SetToSameValue(T),
    // Repeated set, but the value was different (returns an error to the
    // caller)
    AttemptedSetToNewValue(T),
}

ringbuf!(Trace, 16, Trace::None);
#[export_name = "main"]
fn main() -> ! {
    struct StaticBufs {
        mac_address_block: Option<MacAddressBlock>,
        identity: Option<OxideIdentity>,
        #[cfg(feature = "gimlet")]
        gimlet_bufs: gimlet::StaticBufs,
        #[cfg(feature = "cosmo")]
        cosmo_bufs: cosmo::StaticBufs,
        #[cfg(feature = "ereport")]
        ereport_bufs: ereport::EreportBufs,
    }
    let StaticBufs {
        ref mut mac_address_block,
        ref mut identity,
        #[cfg(feature = "gimlet")]
        ref mut gimlet_bufs,
        #[cfg(feature = "cosmo")]
        ref mut cosmo_bufs,
        #[cfg(feature = "ereport")]
        ref mut ereport_bufs,
    } = {
        static BUFS: ClaimOnceCell<StaticBufs> =
            ClaimOnceCell::new(StaticBufs {
                mac_address_block: None,
                identity: None,
                #[cfg(feature = "gimlet")]
                gimlet_bufs: gimlet::StaticBufs::new(),
                #[cfg(feature = "cosmo")]
                cosmo_bufs: cosmo::StaticBufs::new(),
                #[cfg(feature = "ereport")]
                ereport_bufs: ereport::EreportBufs::new(),
            });
        BUFS.claim()
    };

    let mut server = ServerImpl {
        mac_address_block,
        identity,
        #[cfg(feature = "gimlet")]
        gimlet_data: gimlet::GimletData::new(gimlet_bufs),
        #[cfg(feature = "grapefruit")]
        grapefruit_data: grapefruit::GrapefruitData::new(),
        #[cfg(feature = "cosmo")]
        cosmo_data: cosmo::CosmoData::new(cosmo_bufs),
        #[cfg(feature = "ereport")]
        ereport_store: ereport::EreportStore::new(ereport_bufs),
    };

    let mut buffer = [0; idl::INCOMING_SIZE];
    loop {
        idol_runtime::dispatch(&mut buffer, &mut server);
    }
}

struct ServerImpl {
    mac_address_block: &'static mut Option<MacAddressBlock>,
    identity: &'static mut Option<OxideIdentity>,
    #[cfg(feature = "gimlet")]
    gimlet_data: gimlet::GimletData,
    #[cfg(feature = "grapefruit")]
    grapefruit_data: grapefruit::GrapefruitData,
    #[cfg(feature = "cosmo")]
    cosmo_data: cosmo::CosmoData,
    #[cfg(feature = "ereport")]
    ereport_store: ereport::EreportStore,
}

impl ServerImpl {
    // Implementation for properties that may only be set once (e.g., our MAC
    // address block). If `storage` is already `Some(_)`, we log the extra set
    // and return an error if `value` doesn't match.
    fn set_once<T>(
        storage: &mut Option<T>,
        value: T,
    ) -> Result<(), CacheSetError>
    where
        Trace: From<TraceSet<T>>,
        T: PartialEq + Copy,
    {
        match storage {
            Some(prev) => {
                if *prev == value {
                    ringbuf_entry!(TraceSet::SetToSameValue(value).into());

                    // TODO Is this the right return value? Does a caller care
                    // if their set was actually ignored because we already had
                    // the value cached?
                    Ok(())
                } else {
                    ringbuf_entry!(
                        TraceSet::AttemptedSetToNewValue(value).into()
                    );
                    Err(CacheSetError::ValueAlreadySet)
                }
            }
            None => {
                ringbuf_entry!(TraceSet::Set(value).into());
                *storage = Some(value);
                Ok(())
            }
        }
    }
}

#[cfg(feature = "gimlet")]
impl ServerImpl {
    fn spd(&self) -> Option<&SpdData> {
        Some(self.gimlet_data.spd())
    }

    fn spd_mut(&mut self) -> Option<&mut SpdData> {
        Some(self.gimlet_data.spd_mut())
    }
}

#[cfg(feature = "cosmo")]
impl ServerImpl {
    fn spd(&self) -> Option<&SpdData> {
        Some(self.cosmo_data.spd())
    }

    fn spd_mut(&mut self) -> Option<&mut SpdData> {
        Some(self.cosmo_data.spd_mut())
    }
}

#[cfg(not(any(feature = "cosmo", feature = "gimlet")))]
impl ServerImpl {
    fn spd(&self) -> Option<&SpdData> {
        None
    }
    fn spd_mut(&mut self) -> Option<&mut SpdData> {
        None
    }
}

impl idl::InOrderPackratImpl for ServerImpl {
    fn get_mac_address_block(
        &mut self,
        _: &RecvMessage,
    ) -> Result<MacAddressBlock, RequestError<CacheGetError>> {
        let addrs = self.mac_address_block.ok_or(CacheGetError::ValueNotSet)?;
        Ok(addrs)
    }

    fn set_mac_address_block(
        &mut self,
        _: &RecvMessage,
        macs: MacAddressBlock,
    ) -> Result<(), RequestError<CacheSetError>> {
        Self::set_once(self.mac_address_block, macs).map_err(Into::into)
    }

    fn get_identity(
        &mut self,
        _: &RecvMessage,
    ) -> Result<OxideIdentity, RequestError<CacheGetError>> {
        let addrs = self.identity.ok_or(CacheGetError::ValueNotSet)?;
        Ok(addrs)
    }

    fn set_identity(
        &mut self,
        _: &RecvMessage,
        identity: OxideIdentity,
    ) -> Result<(), RequestError<CacheSetError>> {
        Self::set_once(self.identity, identity).map_err(Into::into)
    }

    #[cfg(feature = "gimlet")]
    fn get_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
    ) -> Result<HostStartupOptions, RequestError<Infallible>> {
        Ok(self.gimlet_data.host_startup_options())
    }

    #[cfg(feature = "grapefruit")]
    fn get_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
    ) -> Result<HostStartupOptions, RequestError<Infallible>> {
        Ok(self.grapefruit_data.host_startup_options())
    }

    #[cfg(feature = "cosmo")]
    fn get_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
    ) -> Result<HostStartupOptions, RequestError<Infallible>> {
        Ok(self.cosmo_data.host_startup_options())
    }

    #[cfg(not(any(
        feature = "gimlet",
        feature = "grapefruit",
        feature = "cosmo"
    )))]
    fn get_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
    ) -> Result<HostStartupOptions, RequestError<Infallible>> {
        Err(RequestError::Fail(
            idol_runtime::ClientError::BadMessageContents,
        ))
    }

    #[cfg(feature = "gimlet")]
    fn set_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
        host_startup_options: HostStartupOptions,
    ) -> Result<(), RequestError<Infallible>> {
        ringbuf_entry!(Trace::SetNextBootHostStartupOptions(
            host_startup_options
        ));
        self.gimlet_data
            .set_host_startup_options(host_startup_options);
        Ok(())
    }

    #[cfg(feature = "grapefruit")]
    fn set_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
        host_startup_options: HostStartupOptions,
    ) -> Result<(), RequestError<Infallible>> {
        ringbuf_entry!(Trace::SetNextBootHostStartupOptions(
            host_startup_options
        ));
        self.grapefruit_data
            .set_host_startup_options(host_startup_options);
        Ok(())
    }

    #[cfg(feature = "cosmo")]
    fn set_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
        host_startup_options: HostStartupOptions,
    ) -> Result<(), RequestError<Infallible>> {
        ringbuf_entry!(Trace::SetNextBootHostStartupOptions(
            host_startup_options
        ));
        self.cosmo_data
            .set_host_startup_options(host_startup_options);
        Ok(())
    }

    #[cfg(not(any(
        feature = "gimlet",
        feature = "cosmo",
        feature = "grapefruit"
    )))]
    fn set_next_boot_host_startup_options(
        &mut self,
        _: &RecvMessage,
        _host_startup_options: HostStartupOptions,
    ) -> Result<(), RequestError<Infallible>> {
        Err(RequestError::Fail(
            idol_runtime::ClientError::BadMessageContents,
        ))
    }

    fn set_spd_eeprom(
        &mut self,
        _: &RecvMessage,
        index: u8,
        offset: usize,
        data: LenLimit<Leased<idol_runtime::R, [u8]>, 256>,
    ) -> Result<(), RequestError<Infallible>> {
        if let Some(spd) = self.spd_mut() {
            spd.set_eeprom(index, offset, data)
        } else {
            Err(RequestError::Fail(
                idol_runtime::ClientError::BadMessageContents,
            ))
        }
    }

    fn remove_spd(
        &mut self,
        _: &RecvMessage,
        index: u8,
    ) -> Result<(), RequestError<Infallible>> {
        if let Some(spd) = self.spd_mut() {
            spd.remove_eeprom(index)
        } else {
            Err(RequestError::Fail(
                idol_runtime::ClientError::BadMessageContents,
            ))
        }
    }

    fn get_spd_present(
        &mut self,
        _: &RecvMessage,
        index: u8,
    ) -> Result<bool, RequestError<Infallible>> {
        if let Some(spd) = self.spd() {
            spd.get_present(index)
        } else {
            Err(RequestError::Fail(
                idol_runtime::ClientError::BadMessageContents,
            ))
        }
    }

    fn get_spd_data(
        &mut self,
        _: &RecvMessage,
        index: u8,
        offset: usize,
    ) -> Result<u8, RequestError<Infallible>> {
        if let Some(spd) = self.spd() {
            spd.get_data(index, offset)
        } else {
            Err(RequestError::Fail(
                idol_runtime::ClientError::BadMessageContents,
            ))
        }
    }

    fn get_full_spd_data(
        &mut self,
        _: &RecvMessage,
        index: u8,
        out: Leased<idol_runtime::W, [u8]>,
    ) -> Result<(), RequestError<Infallible>> {
        if let Some(spd) = self.spd() {
            spd.get_full_data(index, out)
        } else {
            Err(RequestError::Fail(
                idol_runtime::ClientError::BadMessageContents,
            ))
        }
    }

    #[cfg(not(feature = "ereport"))]
    fn set_ereport_restart_id(
        &mut self,
        _: &RecvMessage,
        _: u128,
    ) -> Result<(), RequestError<CacheSetError>> {
        Ok(())
    }

    #[cfg(feature = "ereport")]
    fn set_ereport_restart_id(
        &mut self,
        _: &RecvMessage,
        value: u128,
    ) -> Result<(), RequestError<CacheSetError>> {
        let restart_id = ereport_messages::RestartId::new(value);
        Self::set_once(&mut self.ereport_store.restart_id, restart_id)
            .map_err(Into::into)
    }

    #[cfg(not(feature = "ereport"))]
    fn deliver_ereport(
        &mut self,
        _: &RecvMessage,
        _: LenLimit<Leased<idol_runtime::R, [u8]>, 1024usize>,
    ) -> Result<(), RequestError<EreportWriteError>> {
        // go away, we don't know how to do that
        Err(idol_runtime::ClientError::UnknownOperation.fail())
    }

    #[cfg(feature = "ereport")]
    fn deliver_ereport(
        &mut self,
        msg: &RecvMessage,
        data: LenLimit<Leased<idol_runtime::R, [u8]>, 1024usize>,
    ) -> Result<(), RequestError<EreportWriteError>> {
        self.ereport_store.deliver_ereport(msg, data)
    }

    #[cfg(not(feature = "ereport"))]
    fn read_ereports(
        &mut self,
        _msg: &RecvMessage,
        _: ereport_messages::RequestIdV0,
        _: ereport_messages::RestartId,
        _: ereport_messages::Ena,
        _: u8,
        _: ereport_messages::Ena,
        _: Leased<idol_runtime::W, [u8]>,
    ) -> Result<usize, RequestError<EreportReadError>> {
        // go away, we don't know how to do that
        Err(idol_runtime::ClientError::UnknownOperation.fail())
    }

    #[cfg(feature = "ereport")]
    fn read_ereports(
        &mut self,
        _msg: &RecvMessage,
        request_id: ereport_messages::RequestIdV0,
        restart_id: ereport_messages::RestartId,
        begin_ena: ereport_messages::Ena,
        limit: u8,
        committed_ena: ereport_messages::Ena,
        data: Leased<idol_runtime::W, [u8]>,
    ) -> Result<usize, RequestError<EreportReadError>> {
        self.ereport_store.read_ereports(
            request_id,
            restart_id,
            begin_ena,
            limit,
            committed_ena,
            data,
            self.identity.as_ref(),
        )
    }
}

impl NotificationHandler for ServerImpl {
    fn current_notification_mask(&self) -> u32 {
        // We don't use notifications, don't listen for any.
        0
    }

    fn handle_notification(&mut self, _bits: userlib::NotificationBits) {
        unreachable!()
    }
}

mod idl {
    use super::{
        ereport_messages, CacheGetError, CacheSetError, EreportReadError,
        EreportWriteError, HostStartupOptions, MacAddressBlock, OxideIdentity,
    };

    include!(concat!(env!("OUT_DIR"), "/server_stub.rs"));
}
